package first.group.ssh.gui.state;

import com.jediterm.core.util.TermSize;
import com.jediterm.terminal.TtyConnector;
import first.group.ssh.Connection;
import first.group.ssh.ConnectionService;
import first.group.ssh.authorize.Authorizer.AuthorizeListener;
import first.group.ssh.channel.Channel;
import first.group.ssh.channel.session.PtyRequest;
import first.group.ssh.channel.session.SessionChannel;
import first.group.ssh.channel.session.ShellRequest;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.LinkedList;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jetbrains.annotations.NotNull;

public class SshSession implements TtyConnector {

    private static final Logger logger = Logger.getLogger("SshSession");

    public final InetSocketAddress address;
    public final Connection connection;
    private final SessionChannel channel;
    private String failMessage = null;
    private PipedInputStream iStream = new PipedInputStream();
    private InputStreamReader reader = new InputStreamReader(iStream, StandardCharsets.UTF_8);
    private PipedOutputStream oStream = new PipedOutputStream();
    private OutputStreamWriter writer = new OutputStreamWriter(oStream, StandardCharsets.UTF_8);
    
    public SshSession(InetSocketAddress address) {
        this.address = address;
        this.connection = new Connection(address);
        channel = new SessionChannel();
        try {
            iStream.connect(oStream);
        } catch (IOException ex) {
            fail(ex);
            return;
        }
        setupConnection();
    }

    private void setupConnection() {
        connection.addStatusListener(connectionStatus -> {
            switch (connectionStatus) {
                case CONNECTING -> {
                    setStatus(Status.CONNECTING);
                }
                case VERSION_EXCHANGING, KEY_ALGO_NEGOTIATING, KEY_EXCHANGING -> {
                    setStatus(Status.CONNECTING);
                }
                case AUTHORIZING -> {
                    setStatus(Status.AUTHENTICATING);
                }
                case AUTHORIZED -> {
                    setStatus(Status.OPENING_SESSION);
                    connection.openChannel(channel);
                }
                case DISCONNECTING -> {
                    setStatus(Status.DISCONNECTING);
                }
                case DISCONNECTED -> {
                    setStatus(Status.DISCONNECTED);
                }
                case FAILED -> {
                    fail(connection.getFailedException());
                }
                default -> throw new AssertionError(connectionStatus.name());
            }
        });
        channel.addListener(new SessionChannel.StatusListener() {
            @Override
            public void onStatusChanged(Channel.Status status) {
                switch (status) {
                    case NOT_OPENED, OPENING -> {
                        setStatus(Status.OPENING_SESSION);
                    }
                    case OPENED -> {
                        channel.sendRequest(new PtyRequest(80, 24), null);
                        channel.sendRequest(new ShellRequest(), null);
                        setStatus(Status.SESSION_OPENED);
                    }
                    case OPEN_FAILED -> {
                        setStatus(Status.FAILED);
                        fail("Session open failed");
                    }
                    case CLOSE_FLUSHING, CLOSING -> {
                        setStatus(Status.DISCONNECTING);
                    }
                    case CLOSED -> {
                        setStatus(Status.DISCONNECTING);
                        disconnect();
                    }
                    default -> throw new AssertionError(status.name());
                }
            }

            @Override
            public void onLocalEof() {}

            @Override
            public void onRemoteEof() {}
        });
        channel.setCallback(new SessionChannel.SessionChannelDataCallback() {
            @Override
            public void readData(byte[] data) {
                try {
                    oStream.write(data);
                    oStream.flush();
                } catch (IOException ex) {
                    logger.log(Level.SEVERE, null, ex);
                }
            }

            @Override
            public void readExtendedData(byte[] data) {
                try {
                    oStream.write(data);
                    oStream.flush();
                } catch (IOException ex) {
                    logger.log(Level.SEVERE, null, ex);
                }
            }
        });
    }

    public enum Status {
        NOT_CONNECTED,
        CONNECTING,
        AUTHENTICATING,
        OPENING_SESSION,
        SESSION_OPENED,
        DISCONNECTING,
        DISCONNECTED,
        FAILED
    }

    public interface StatusListener {
        void onStatusChanged(Status status);
    }

    private Status status = Status.NOT_CONNECTED;
    private final LinkedList<StatusListener> listeners = new LinkedList<>();

    public void addListener(StatusListener listener) {
        listeners.add(listener);
    }

    private void setStatus(Status status) {
        if (this.status == status) {
            return;
        }
        this.status = status;
        for (var listener : listeners) {
            listener.onStatusChanged(status);
        }
    }

    public void open() {
        if (status != Status.NOT_CONNECTED) {
            throw new IllegalStateException("");
        }
        try {
            ConnectionService.getInstance().addConnection(connection);
        } catch (Exception ex) {
            setStatus(Status.FAILED);
            fail(ex);
        }
    }
    
    public void setAuthListener(AuthorizeListener listener) {
        connection.setAuthorizeListener(listener);
    }
    
    public void putMessageLine(String message) {
        putMessage(message.replace("\n", "\r\n"));
        putMessage("\r\n");
    }
    
    
    public void putMessage(String message) {
        try {
            writer.write(message);
            writer.flush();
        } catch (IOException ex) {
            Logger.getLogger(SshSession.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    
    public void disconnect() {
        channel.close();
        connection.disconnect();
    }
    
    private void fail(Throwable exception) {
        if (exception == null) {
            fail((String) null);
            return;
        }
        var strWriter = new StringWriter();
        exception.printStackTrace(new PrintWriter(strWriter));
        fail(strWriter.toString());
    }
    
    private void fail(String message) {
        failMessage = message;
        setStatus(Status.FAILED);
    }
    
    public String getFailMessage() {
        return failMessage;
    }

    @Override
    public int read(char[] chars, int off, int len) throws IOException {
        return reader.read(chars, off, len);
    }

    @Override
    public void write(byte[] bytes) throws IOException {
        if (status != Status.SESSION_OPENED) {
            return;
        }
        channel.writeChannelData(bytes);
    }

    @Override
    public void write(String s) throws IOException {
        write(s.getBytes(StandardCharsets.UTF_8));
    }

    @Override
    public boolean isConnected() {
        return true;
    }

    @Override
    public void resize(@NotNull TermSize termSize) {
        TtyConnector.super.resize(termSize);
    }

    @Override
    public int waitFor() throws InterruptedException {
        return 0;
    }

    @Override
    public boolean ready() throws IOException {
        return reader.ready();
    }

    @Override
    public String getName() {
        return "SSHSession";
    }

    @Override
    public void close() {
        disconnect();
    }
}
